/*
 *	CODESPACE ++
 *	A real-time syntax highlighting javascript editor
 *
 *	Copyright (C) 2008 Francois Botman, <Franchie.brightspark@gmail.com>
 *
 *	This script is licenced under: Creative Commons Attribution-Noncommercial-Share Alike 3.0
 *	(see http://creativecommons.org/licenses/by-nc-sa/3.0/).
 *	It bascially means that it can be modified and shared, provided it is
 *	clearly attributed, and can be used in personnal/charity projects for free!
 *	(Commercial use requires prior negotiation with the author).
 *	In any case, should anything go wrong, I cannot be held responsible!
 *
 *	Based on an original idea by Fernando M.A.d.S.,  http://codepress.org/
 *
 *	-----------
 *
 *	This file contains functions that create the actual editor, they should not be invoked
 *	directly, but through the codespace constructor interface.
 *	This file only handles:
 *		- line/row management
 *		- keyboard input
 *		- focus (click)
 *		- (non-visulal) cursor
 */

String.prototype.insert = function(index, c) {
	var a = this.slice(0, index);
	var b = this.slice(index, this.length);
	return a.concat(c).concat(b);
}
String.prototype.remove = function(index, num) {
	var a = this.slice(0, index);
	var b = this.slice((index+num>this.length)?this.length:(index+num), this.length);
	return a.concat(b);
}
Array.prototype.remove = function(what) {
	var where = this.indexOf(what);
	if(where<0) return;
	this.splice(where, 1);
}
Array.prototype.insertAt = function(what, index){    
	if(index > -1 && index <= this.length ) {
		this.splice(index, 0, what);
		return true;
	}        
	return false;
}
Array.prototype.insertBefore = function(reference, toInsert){
	var inserted = false;
	var index = this.indexOf(reference);
	if(index == -1) return false;
	else return ((index == 0) ? this.unshift(toInsert) : this.insertAt(toInsert, index - 1));
}
Array.prototype.insertAfter = function(reference, toInsert){
	var inserted = false;
	var index = this.indexOf(reference);
	if(index == -1) return false;
	else return ((index == this.length - 1) ? this.push(toInsert) : this.insertAt(toInsert, index + 1));
}



/*
 * A handy function to get the position of an element...
 */
function getPosition(element)
{
	var t = element, x = 0, y = 0;
	if(!t) return {x:0,y:0,w:0,h:0};
	do {x += t.offsetLeft; y += t.offsetTop;} while(t = t.offsetParent);
	return {x:x, y:y, w:element.offsetWidth, h:element.offsetHeight};
}

/*
 * And another nice element that ensures that the element is visible within all of its
 * parent elements. This means that it will basically scroll into view, starting from
 * its direct parent and climbing.
 *
 * The 'rightborder' parameter can be set to indicate that the right side of the element,
 * rather than the left, should be used to calculate the position.
 *
 * It also hard-codes a 'sticky margin', which forces back the scroll to one of the extremes
 * when it gets close enough...
 */
function makeVisible(element, rightborder)
{
return;
	if(!element || !element.parentNode) return;
	var margin = 10;
	var stickymargin = {x:150, y:50};
	var parent = element.parentNode;
	var cpos = getPosition(element);	// cursor position
	if(rightborder) cpos.x = cpos.x + cpos.w;
	while(parent) {
		var ppos = getPosition(parent);// parent position
		var vpos = {x:parent.scrollLeft, y:parent.scrollTop, h:parent.clientHeight, w:parent.clientWidth};	// parent viewport
		if(cpos.x < (ppos.x+vpos.x+margin)) {
			parent.scrollLeft = (cpos.x - ppos.x - margin);
			if(parent.scrollLeft < stickymargin.x) parent.scrollLeft = 0;
		}
		else if(cpos.x > (ppos.x+vpos.x+vpos.w-margin)) {
			parent.scrollLeft = (cpos.x-ppos.x-vpos.w+margin);
			if((vpos.w+parent.scrollLeft) > (parent.scrollWidth-stickymargin.x)) parent.scrollLeft = (parent.scrollWidth);
		}
		if(cpos.y < (ppos.y+vpos.y+margin)) {
			parent.scrollTop = (cpos.y - ppos.y - margin);
			if(parent.scrollTop < stickymargin.y) parent.scrollTop = 0;
		}			
		else if((cpos.y+cpos.h) > (ppos.y+vpos.y+vpos.h-margin)) {
			parent.scrollTop = (cpos.y-ppos.y-vpos.h+margin+cpos.h);
			if((vpos.h+parent.scrollTop) > (parent.scrollHeight-stickymargin.y)) parent.scrollTop = (parent.scrollHeight);
		}
		cpos.x -= parent.scrollLeft;
		cpos.y -= parent.scrollTop;
		if(parent == document.body) return;
		parent = parent.parentNode;
	}
}


/*
 * This is the actual editor componant that will be created.
 * This should not be called directly. Ever. Use the DAPETON constructor instead!
 */
Codespace.Editor = function(api, t, p, o) {
	var parent = p;
	var options = o;
	var self = this;
	var table = t;
	this.table = table;
	this.options = o;
	this.api = api;
	
	/*
	 *	Stores keyboard shortcuts
	 *	These should be of the form:
	 *		{ctrl: true, shift: true, alt: true, key: charcode, chr: '[uppercase character!]', callback: [text or handle]}
	 */
	this.shortcuts = new Array();
	
	/*
	 *	And the same thing for events...
	 *	Current events are:
	 *		- click
	 *		- focus
	 *		- unfocus
	 */
	var events = {
		click: [],
		focus: [],
		unfocus: []
	};
	
	
	/*
	 *	We also need a couple of cursors to keep us company!
	 */
	this.caret = null;
	this.selectCaret = null;
	
	
	
	
	// ********   PRIVATE FUNCTIONS   *************************************************************************************************************
	
	/*
	 *	This function handles the changes in styles/classes when the focus of the editor is gained/lost.
	 */
	function changeFocus(focus) {
		if(focus) {
			if(!self.focus) {
				table.className = table.className.replace(/unfocused/gi, '')+' focused';
				self.fixed_reference_div.style.display = 'block';
				self.selection.update();
				invokeEvent('focus');
			}
			self.focus = true;
		}
		else {
			self.focus = false;
			table.className = table.className.replace(/(un)?focused/gi, '')+' unfocused';
			self.caret.obj.parentNode.parentNode.className = self.caret.obj.parentNode.parentNode.className.replace(/selected_row/gi, '');
			self.fixed_reference_div.style.display = 'none';
			self.process_mouse_up = null;
			invokeEvent('unfocus');
		}
	}
	
	/*
	 *	This is invoked when the user clicks.
	 *	When the click is in the table, the editor is selected, and deselected otherwise
	 */
	function mouseHandler(evt) {
		evt = evt || window.event;
		var target = evt.target || evt.srcElement;
		self.allow_dragging = false;
		if(!self.process_mouse_up && evt.type == "mouseup") return;
		
		var t = target;
		if(t && !options.readOnly) {
			do {
				if(t == document.body) {self.focus = false;break;}
				
				// Does the click belong to our editor?
				if(t == parent) {changeFocus(true);break;}
			} while(t = t.parentNode);
		}
		
		if(!self.focus || !t || options.readOnly) {changeFocus(false);return true;}
		else {
			// Do stuff here...
			if(evt.type == "mousedown") {
				invokeEvent('click');
				self.allow_dragging = true;
			}
			//if(Codespace.RowClick) Codespace.RowClick(evt, target, self);
			
			// Get the clicked row's object, and invoke the onclick function
			var e = target;
			while(e) {
				if(e.lineobj != null) {// && e.tagName.match(/tr/i)) {
					var lo = e.lineobj;
					//if(typeof(lo) == 'number') lo = self.table.rows[lo].line;
					lo.onclick(evt, target);
					self.selection.update();
					break;
				}
				e = e.parentNode;
			}
		
			// Cancel event
			if(evt.preventDefault) evt.preventDefault();						// Gecko and Others
			evt.returnValue = false;											// IE
			self.process_mouse_up = null;
			return false;
		}
	}
	
	
	
	/*
	 *	This is invoked when the user presses a key. Any key. Anywhere.
	 *	When it is able to handle the key, the event is blocked.
	 */
	var key_processed = false;
	function keyHandler(evt) {
		if(!self.focus || options.readOnly) return true;
		evt = evt || window.event;
		var chr;
		
		/*
		 *	The characters can now be correctly understood
		 *	Handle all "normal" keys during a keypress event
		 */
		if(evt.type == "keypress") {
			if(!key_processed) {
				if(!evt.which) chr = String.fromCharCode(evt.keyCode);  			// IE
				else if(evt.which > 0) chr = String.fromCharCode(evt.which);		// All others
				else return true;
				if(key) key(chr);
				if(evt.preventDefault) evt.preventDefault();						// Gecko and Others
				evt.returnValue = false;											// IE
				return false;
			}
			else {key_processed = false;return true;}
		}
		
		/*
		 *	This is called first, but with less information under most browsers...
		 *	Handle special keys and shortcuts here...
		 */
		else {
			var fromChar = String.fromCharCode( evt.charCode || evt.keyCode );
			key_processed = false;
			
			// Search for any shortcuts, and stop the flow if needed...
			for(var i=0; i<self.shortcuts.length; i++) {
				var a = self.shortcuts[i];
				if((!a.key || a.key == evt.keyCode) && (!a.chr || a.chr == fromChar) && (a.shift==null || a.shift == evt.shiftKey) &&
				(a.ctrl==null || a.ctrl == evt.ctrlKey || a.ctrl == evt.metaKey) && (a.alt==null || a.alt == evt.altKey)) {
					try {
						if(shortcut) key_processed = (shortcut(a.callback, a)) || key_processed;
						else key_processed = true;
					} catch(e) {
						if(window.console) console.error('Shortcut callback function error: '+e);
					}
				}
			}
			
			// Handle problematic "normal" characters here...
			if(!key_processed) {
				key_processed = true;
				switch(evt.keyCode) {
				case 9:		// TAB
					if(key) key('\t');
					break;
				default:
					key_processed = false;
					break;
				}
			}
			
			// And prevent the default action only if necessary
			if(key_processed) {
				if(evt.preventDefault) evt.preventDefault();						// Gecko and Others
				evt.returnValue = false;											// IE
				return false;
			}
			else if((evt.keyCode && evt.keyCode>=112 && evt.keyCode<=123) ||		// Avoid F1-F12 keys
			(evt.ctrlKey && !evt.altKey)											// CTRL key is pressed and not alt key
			) {
				key_processed = true;
				return false;
			}
		}
		return true;
	}
	
	// Which invokes this when a 'normal' key is encountered:
	function key(c) {
		api.insertText(c);
		//self.caret.insert(c);
		//self.selectCaret.move(self.caret.line, self.caret.pos);
	}
	
	// And this is invoked on a "shortcut". Should return TRUE if the shortcut is processed...
	function shortcut(callback, c) {
		return callback();
	}

	// This invokes an event handler
	function invokeEvent(event) {
		for(var i=0, l = events[event].length; i<l; i++) if(events[event][i]) events[event][i](event);
	}
	
	// ********   SEMI-PUBLIC FUNCTIONS   *******************************************************************************************************
	/*
	 *	Registers a callback handler for the specified event.
	 *	Returns 1 on error, 0 on success
	 */
	this.registerHandler = function(event, callback) {
		if(events[event] && callback) {
			events[event].push(callback);
			return 0;
		}
		return 1;
	}
	this.unregisterHandler = function(event, callback) {
		for(var i=0;i<events[event].length;i++) if(events[event][i] == callback) delete events[event][i];
	}
	
	// ********   PUBLIC FUNCTIONS   *************************************************************************************************************
	/*
	 *	This sets the text of the editor, replacing anything there before...
	 */
	
	/*
	 *	This inserts text at the current cursor position.
	 *	If there is a selection, this text will replace it.
	 */
	api.insertText = function(t) {
		// TODO: replace selection
		if(!t) return;
		t = t.replace(/\r/g, '');
		var line = self.caret.obj.lineobj;
		var text = t.split(/\n/gi);
		for(var i=0; i<text.length; i++) {
			if(i) {
				line.insertAfter();
				line = line.next;
			}
			if(text[i]) line.insertText(text[i]);
		}
		self.selectCaret.obj = self.caret.obj;
		self.selectCaret.objOffset = self.caret.objOffset;
		self.selection.update();
	}
	
	/*
	 *	This returns a handle of an absolutely positioned div which moves in symbiosis with the table.
	 */
	api.getFixedReference = function() {
		if(!self.fixed_reference_div) {
			self.fixed_reference_div = document.createElement('div');
			self.fixed_reference_div.style.position = 'relative';
			self.fixed_reference_div.style.width = '0px';
			self.fixed_reference_div.style.height = '0px';
			parent.insertBefore(self.fixed_reference_div, table);
		}
		var a = self.fixed_reference_div.appendChild(document.createElement('div'));
		a.style.position = 'absolute';
		return a;
	}
	
	/*
	 *	Manual focusing... 
	 */
	api.unfocus = function() {changeFocus(false);}
	api.focus = function() {changeFocus(true);}
	
	// ********   CONSTRUCTOR FUNCTIONS   ********************************************************************************************************

	// Create the cursor objects
	this.caret = new Codespace.Caret(self);
	this.selectCaret = new Codespace.Caret(self);
	
	// Create a selection object
	if(Codespace.Selection) this.selection = new Codespace.Selection(self);
	
	// Create a new empty line
	new Codespace.Line(0, null, null, null, self);

	
	// If the table is disabled, lets disable it!
	if(options.readOnly) table.className += ' readonly';
	
	// Add some callbacks for a sense of interactiveness
	if(document.attachEvent) document.attachEvent('onmousedown', mouseHandler);
	else document.addEventListener('mousedown', mouseHandler, false);
	if(document.attachEvent) document.attachEvent('onmouseup', mouseHandler);
	else document.addEventListener('mouseup', mouseHandler, false);
	
	/*
	 *	If we are under IE, there is a bug that does not send a mousedown signal when a double-click is detected.
	 *	Therefore, specifically under this browser, we must consider a 'doubleclick' to be equivalent to the missing click
	 *	sent by other browsers...
	 */
	if(navigator.userAgent.match(/msie/i)) {
		if(document.attachEvent) document.attachEvent('ondblclick', mouseHandler);
		else document.addEventListener('dblclick', mouseHandler, false);
		window.isIE = true;
	}
	
	/*
	 *	Next handle the key-catching callbacks.
	 *	This is pretty important (for a text editor)
	 */
	if(document.attachEvent) document.attachEvent('onkeypress', keyHandler);
	else document.addEventListener('keypress', keyHandler, false);
	if(document.attachEvent) document.attachEvent('onkeydown', keyHandler);
	else document.addEventListener('keydown', keyHandler, false);
	
	
	/*
	 *	Now register ourselves some "shortcuts"
	 *	These either map to standard keys, or perform special features that should be implemented as default
	 */
	this.shortcuts.push({key: 39, callback: function(){	// RIGHT ARROW
		self.caret.moveRelative(+1);
		self.selectCaret.moveRelative(+1);
		self.selection.update();
		return true;
	}});
	
	this.shortcuts.push({key: 37, callback: function(){	// LEFT ARROW
		self.caret.moveRelative(-1);
		self.selectCaret.moveRelative(-1);
		self.selection.update();
		return true;
	}});
	
	this.shortcuts.push({key: 38, callback: function(){	// UP ARROW
		self.caret.moveDown(-1);
		self.selectCaret.moveDown(-1);
		self.selection.update();
		return true;
	}});
	
	this.shortcuts.push({key: 40, callback: function(){	// DOWN ARROW
		self.caret.moveDown(+1);
		self.selectCaret.moveDown(+1);
		self.selection.update();
		return true;
	}});
	
	this.shortcuts.push({key: 13, callback: function(){key('\n');return true;}});	// RETURN / ENTER
	
	
	
	
	api.insertText(options.text);
	self.caret.move(0,0);
	self.selectCaret.move(0,0);
	
	
	// Next change the string given... if this was long, it could free some meory!
	options.text = '';
	
	// debug
	window.ed = self;
}
